// @author A0096579E

package rightnow.storage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.eaio.uuid.UUID;

/**
 * StorageImpl is the implementation of the Storage java interface
 */
public class StorageImpl implements Storage {

	private static final String CMD_TEMPLATE = "%1$s \"%2$s\"";
	
	private static final String ADD = "add";
	private static final String DELETE = "delete";
	private static final String EDIT = "edit";
	private static final String PIN = "pin";
	private static final String UNPIN = "unpin";
	private static final String DONE = "done";
	private static final String UNDONE = "undone";
	private static final String DELETE_DONE = "delete done";
	
	private final Logger log = LoggerFactory.getLogger(StorageImpl.class);

	private HashMap<UUID, Task> hashMap;
	private StorageStateHolder stateHolder;
	private String stateChange;

	public StorageImpl() {
		this.hashMap = new HashMap<UUID, Task>();
		this.stateHolder = new StorageStateHolder();
	}

	@Override
	public UUID addTask(Task task) {
		assert task.getTaskId() == null;
		
		updateStateHolder();
		
		stateChange = String.format(CMD_TEMPLATE, ADD, task.getDescription());
		assignTaskId(task);
		hashMap.put(task.getTaskId(), task);
		
		log.debug("Added: " + task.getTaskId());

		return task.getTaskId();
	}

	@Override
	public void deleteTask(UUID taskId) {
		assert hashMap.containsKey(taskId);
		
		updateStateHolder();
		
		Task taskToDelete = hashMap.get(taskId);
		stateChange = String.format(CMD_TEMPLATE, DELETE, taskToDelete.getDescription());
		hashMap.remove(taskId);
		
		log.debug("Removed: " + taskId);
	}

	@Override
	public void editTask(UUID taskId, Task task) {
		assert hashMap.containsKey(taskId);
		
		updateStateHolder();
		
		Task taskToEdit = hashMap.get(taskId);
		stateChange = String.format(CMD_TEMPLATE, EDIT, taskToEdit.getDescription());
		setProtectedTaskAttributes(taskId, task, taskToEdit);
		hashMap.put(task.getTaskId(), task);
		
		log.debug("Edited: " + task.getTaskId());
	}

	@Override
	public void pinTask(UUID taskId) {
		assert hashMap.containsKey(taskId);
		
		updateStateHolder();
		
		Task taskToPin = hashMap.get(taskId);
		stateChange = String.format(CMD_TEMPLATE, PIN, taskToPin.getDescription());
		Task taskPinned = copyAndSetPinned(taskToPin, true);
		hashMap.put(taskId, taskPinned);
		
		log.debug("Pinned: " + taskId);
	}
	
	@Override
	public void unpinTask(UUID taskId) {
		assert hashMap.containsKey(taskId);
		
		updateStateHolder();
		
		Task taskToUnpin = hashMap.get(taskId);
		stateChange = String.format(CMD_TEMPLATE, UNPIN, taskToUnpin.getDescription());
		Task taskUnpinned = copyAndSetPinned(taskToUnpin, false);
		hashMap.put(taskId, taskUnpinned);
		
		log.debug("Unpinned: " + taskId);
	}
	
	@Override
	public void doneTask(UUID taskId) {
		assert hashMap.containsKey(taskId);
		
		updateStateHolder();
		
		Task taskToDone = hashMap.get(taskId);
		stateChange = String.format(CMD_TEMPLATE, DONE, taskToDone.getDescription());
		Task taskDone = copyAndSetDone(taskToDone, true);
		hashMap.put(taskId, taskDone);
		
		log.debug("Done: " + taskId);
	}
	
	@Override
	public void undoneTask(UUID taskId) {
		assert hashMap.containsKey(taskId);
		
		updateStateHolder();
		
		Task taskToUndone = hashMap.get(taskId);
		stateChange = String.format(CMD_TEMPLATE, UNDONE, taskToUndone.getDescription());
		Task taskUndone = copyAndSetDone(taskToUndone, false);
		hashMap.put(taskId, taskUndone);
		
		log.debug("Undone: " + taskId);
	}
	
	@Override
	public void deleteDoneTasks() {
		updateStateHolder();
		
		stateChange = DELETE_DONE;
		removeDoneTasks();
		
		log.debug("Removed all done tasks");
	}

	@Override
	public List<Task> allTasksToList() {
		Collection<Task> collection = hashMap.values();
		List<Task> returnedList = new ArrayList<Task>(collection);
		return returnedList;
	}

	@Override
	public List<Task> matchedTasksToList(String searchTerms) {
		List<Task> mainList = allTasksToList();
		String[] searchTermTokens = getTokens(searchTerms);
		List<Task> returnedList = retrieveMatchedTasks(searchTermTokens, mainList);
		return returnedList;
	}

	@Override
	public List<Task> nowTasksToList(DateTime dateTime) {
		List<Task> mainList = allTasksToList();
		List<Task> returnedList = retrieveCoincidingTasks(dateTime, mainList);
		return returnedList;
	}

	@Override
	public void loadFile() throws IOException {
		hashMap.clear();
		FileHandler.load(hashMap);
		log.debug("File loaded");
	}

	@Override
	public void saveFile() throws IOException {
		FileHandler.save(allTasksToList());
		log.debug("File saved");
	}

	@Override
	public boolean undo() {
		HashMap<UUID, Task> copy = new HashMap<UUID, Task>(hashMap);
		StorageState currentState = new StorageState(copy, stateChange);
		StorageState nextUndoState = stateHolder.getUndoState(currentState);
		
		if (nextUndoState != null) {
			hashMap = nextUndoState.getState();
			stateChange = nextUndoState.getStateMessage();
			log.debug("Undo Successful");
			return true;
		}
		log.debug("Undo Failed");
		return false;
	}

	@Override
	public boolean redo() {
		HashMap<UUID, Task> copy = new HashMap<UUID, Task>(hashMap);
		StorageState currentState = new StorageState(copy, stateChange);
		StorageState nextRedoState = stateHolder.getRedoState(currentState);
		
		if (nextRedoState != null) {
			hashMap = nextRedoState.getState();
			stateChange = nextRedoState.getStateMessage();
			log.debug("Redo Successful");
			return true;
		}
		log.debug("Redo Failed");
		return false;
	}
	
	@Override
	public String getStateChange() {
		return this.stateChange;
	}
	
	/*
	 *  These are private methods
	 */
	
	private void assignTaskId(Task task) {
		UUID taskId = new UUID();
		while (hashMap.containsKey(taskId)) {
			taskId = new UUID();
		}
		task.setTaskId(taskId);
	}
	
	private void setProtectedTaskAttributes(UUID taskId, Task task,
			Task taskToEdit) {
		task.setTaskId(taskId);
		task.setDone(taskToEdit.isDone());
		task.setPinned(taskToEdit.isPinned());
	}
	
	private Task copyAndSetPinned(Task taskToSet, boolean pinValue) {
		Task taskSet = new Task(taskToSet);
		taskSet.setPinned(pinValue);
		return taskSet;
	}
	
	private Task copyAndSetDone(Task taskToSet, boolean doneValue) {
		Task taskSet = new Task(taskToSet);
		taskSet.setDone(doneValue);
		return taskSet;
	}
	
	private void removeDoneTasks() {
		List<Task> mainList = allTasksToList();
		for (Task currentTask : mainList) {
			if (currentTask.isDone()) {
				hashMap.remove(currentTask.getTaskId());
			}
		}
	}
	
	private List<Task> retrieveMatchedTasks(String[] searchTermTokens,
			List<Task> mainList) {
		List<Task> returnedList = new ArrayList<Task>();
		
		for (Task currentTask : mainList) {
			String[] descriptionTokens = getTokens(currentTask.getDescription());
			
			boolean isAdded = false;
			for (int i = 0; i < descriptionTokens.length && !isAdded; i++) {
				for (int j = 0; j < searchTermTokens.length && !isAdded; j++) {
					if (descriptionTokens[i].compareToIgnoreCase(searchTermTokens[j]) == 0) {
						returnedList.add(currentTask);
						isAdded = true;
					}
				}
			}
		}
		
		return returnedList;
	}
	
	private List<Task> retrieveCoincidingTasks(DateTime dateTime,
			List<Task> mainList) {
		List<Task> returnedList = new ArrayList<Task>();
		
		for (Task currentTask : mainList) {
			DateTime taskStart = currentTask.getStart();
			DateTime taskEnd = currentTask.getEnd();
			
			if (!currentTask.isDone()) {
				if (currentTask.isPinned()) {
					returnedList.add(currentTask);
					
				} else { // Check if currentTask coincides with dateTime
					if (isOverdue(dateTime, taskEnd)) {
						returnedList.add(currentTask);
						
					} else if (startIsBeforeDTAndNoEnd(dateTime, taskStart, taskEnd)) {
				  	returnedList.add(currentTask);
				  	
					} else if (endIsAfterDTAndNoStart(dateTime, taskStart, taskEnd)) {
						returnedList.add(currentTask);
						
					} else if (startIsBeforeAndEndIsAfterDT(dateTime, taskStart, taskEnd)) {
						returnedList.add(currentTask);
					}
				}
			}
		}
		
		return returnedList;
	}

	private boolean isOverdue(DateTime dateTime, DateTime taskEnd) {
		return taskEnd != null && taskEnd.compareTo(dateTime) < 0;
	}
	
	private boolean startIsBeforeDTAndNoEnd(DateTime dateTime,
			DateTime taskStart, DateTime taskEnd) {
		return (taskStart != null && taskStart.compareTo(dateTime) <= 0) &&
				(taskEnd == null);
	}
	
	private boolean endIsAfterDTAndNoStart(DateTime dateTime, DateTime taskStart,
			DateTime taskEnd) {
		return (taskEnd != null && taskEnd.compareTo(dateTime) >= 0) &&
				(taskStart == null);
	}
	
	private boolean startIsBeforeAndEndIsAfterDT(DateTime dateTime,
			DateTime taskStart, DateTime taskEnd) {
		return (taskStart != null && taskStart.compareTo(dateTime) <= 0) &&
				(taskEnd != null && taskEnd.compareTo(dateTime) >= 0);
	}
	
	private void updateStateHolder() {
		StorageState currentState = new StorageState(new HashMap<UUID, Task>(hashMap), stateChange);
		stateHolder.updateUndoState(currentState);
	}
	
	private String[] getTokens(String string) {
		return string.trim().split("\\s+");
	}

	/*
	 * These are methods used in testing
	 */

	void clearTasks() {
		hashMap.clear();
	}

}
